iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Mobile Development

從開發瀏覽器 APP 學習 Android 實戰技巧,並搭上 Jetpack Compose 的列車系列 第 29

[Day28] 從開發瀏覽器 APP 學習實戰技巧 -- 是時候淘汰 onActivityResult 了

  • 分享至 

  • xImage
  •  

如果是很早之前就開始開發 Android APP 的話,應該對 onActivityResult 都熟到不能再熟。當初 Activity 被設計成是畫面的最小單位,一個 APP 中可以有多個 Acitivity 提供不同的功能。而不同的 Activity 之間可以藉由 intent 叫起其他的 Activity,讓它達成工作後,再把結果帶回前一個 Activity。這時,利用的就是這個 onActivityResult 的 callback 函式。這種跨 Activity 的溝通機制,甚至可以應用在不同 APP 之間的互動。比方說,A 應用程式叫起 B 應用程式提供的 photo picker Activity,在 B 裡選完照片後,再把照片 resource uri 返回給 A 做後續的處理。

曾幾何時, onActivityResult 因為很多因素的考量,漸漸地被廢棄了。這一篇文章要來說說,當 onActivityResult 不再是正解時,我們要怎麼完成同樣的功能。

實作

照慣例,我們用 EinkBro 中的應用場景來解釋。

當使用者覺得網頁字型不是自己喜歡的樣式,想要選擇一個自己放入系統中的字型檔案,EinkBro 會呼叫系統的 file picker 起來;等使用者選完檔案後,file picker 會將結果送回給 EinkBro 的 Activity。我們來看這個功能要怎麼實作。

1. 註冊 ActivityResult

在 BrowserUnit 中,先定義好註冊 AcitivityResult 的函式。在一般情況下,如果沒有特殊需求,我們都會使用 ActivityResultContracts.StartActivityForResult() 這個預設的 Contract,然後再最後的 {} 可以看到:當結果回來時,會在 handleFontSelectionResult() 處理字型的 content uri (將它存入 SharedPreferences)。

另外,這個函式會回傳 ActivityResultLauncher,我們會在需要的時候呼叫它。

fun registerCustomFontSelectionResult(fragment: Fragment): ActivityResultLauncher<Intent> =            
    fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult())     { handleFontSelectionResult(fragment.requireContext(), it) }

private fun handleFontSelectionResult(context: Context, activityResult: ActivityResult) {
    if (activityResult.data == null || activityResult.resultCode != Activity.RESULT_OK) return
    val uri = activityResult.data?.data ?: return

    val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
    context.contentResolver?.takePersistableUriPermission(uri, takeFlags)

    val file = File(uri.path)
    config.customFontInfo = CustomFontInfo(file.name, uri.toString())
}

2. 呼叫上面建立好的 ActivityResultLauncher

BrowserActivity 中,我們先呼叫 1 中的函式,將產生的 ActivityResultLauncer 留著備用。

    private val customFontResultLauncher: ActivityResultLauncher<Intent> =
        BrowserUnit.registerCustomFontSelectionResult(this)

然後,在下面的實作中,我們使用了它。可以看到在呼叫 resultLauncher.launch() 時,需要帶入一個 Intent 。這個 Intent 就是之前用來帶入 startActivityForResult() 的參數。

// BrowserActivity
private fun openCustomFontPicker() = BrowserUnit.openFontFilePicker(customFontResultLauncher)

// BrowserUnit
fun openFontFilePicker(resultLauncher: ActivityResultLauncher<Intent>) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = Constants.MIME_TYPE_ANY
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

    resultLauncher.launch(intent)
}

結語

當系統在進行一些比較吃記憶體的行為時(像是開相機拍照),很有可能你的 process 或 Activity 會因為記憶體不足而被系統砍掉,造成回傳的結果就收不到了。

新個流程雖然比原先的作法複雜了些,又是 Contract 又是 Launcher 的,多了許多新元件。但這麼做的好處是:它把回傳結果的流程和啟動其他 Activity 的實作切開來,避免上面的問題發生。

來自官網的說明:
When starting an activity for a result, it is possible (and, in cases of memory-intensive operations such as camera usage, almost certain) that your process and your activity will be destroyed due to low memory.
For this reason, the Activity Result APIs decouple the result callback from the place in your code where you launch the other activity. As the result callback needs to be available when your process and activity are recreated, the callback must be unconditionally registered every time your activity is created, even if the logic of launching the other activity only happens based on user input or other business logic.

注意事項

建議把生成的 ActivityResultLauncher 包在 lifecycle 中,讓 lifecycle 幫忙處理生命週期。如果無法取得 lifecycle 的話,也可以在不需要時,自行呼叫 ActivityResultLauncherunregister()

相關連結


上一篇
[Day27] 從開發瀏覽器 APP 學習實戰技巧 -- Copilot 初體驗,跟 AI pair programming
下一篇
[Day29] 從開發瀏覽器 APP 學習實戰技巧 -- 追蹤碼退散!享受不受監視的瀏覽體驗
系列文
從開發瀏覽器 APP 學習 Android 實戰技巧,並搭上 Jetpack Compose 的列車31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言